Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 3b8704b4da21f15329fd71b0b2cf876291fead5e


Parents : e363738
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-10T00:46:35-05:00

fix(banishment): block all identity addresses, prevent delivery callbacks, fix unbanish UI sync

Changes

3 files changed, 131 insertions(+), 47 deletions(-)


Diff

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index b9f1f042..b297cb2f 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -11517,6 +11517,22 @@ class ReticulumMeshChat:
try:
self.database.misc.add_blocked_destination(destination_hash)
+ # Block all known destinations for the same identity
+ announce = self.database.announces.get_announce_by_hash(
+ destination_hash
+ )
+ if announce and announce.get("identity_hash"):
+ identity_hash = announce["identity_hash"]
+ other_announces = (
+ self.database.announces.get_announces_by_identity_hash(
+ identity_hash
+ )
+ )
+ for other in other_announces:
+ other_hash = other["destination_hash"]
+ if other_hash != destination_hash:
+ self.database.misc.add_blocked_destination(other_hash)
+ self._lxmf_reticulum_enforce_block(other_hash)
except Exception:
return web.json_response(
{"error": "Destination already blocked"},
@@ -11528,6 +11544,8 @@ class ReticulumMeshChat:
local_hash = self.local_lxmf_destination.hash.hex()
self.message_handler.delete_conversation(local_hash, destination_hash)
+ AsyncUtils.run_async(self._broadcast_blocked_destinations())
+
return web.json_response({"message": "ok"})
# remove blocked destination
@@ -11543,26 +11561,41 @@ class ReticulumMeshChat:
try:
self.database.misc.delete_blocked_destination(destination_hash)
- # remove from Reticulum blackhole if available and enabled
- if self.config.blackhole_integration_enabled.get():
- try:
- if hasattr(self, "reticulum") and self.reticulum:
- # Try to resolve identity hash from destination hash
- identity_hash = None
- announce = self.database.announces.get_announce_by_hash(
- destination_hash,
- )
- if announce and announce.get("identity_hash"):
- identity_hash = announce["identity_hash"]
+ # Unblock all known destinations for the same identity
+ announce = self.database.announces.get_announce_by_hash(
+ destination_hash
+ )
+ if announce and announce.get("identity_hash"):
+ identity_hash = announce["identity_hash"]
+ other_announces = (
+ self.database.announces.get_announces_by_identity_hash(
+ identity_hash
+ )
+ )
+ for other in other_announces:
+ other_hash = other["destination_hash"]
+ if other_hash != destination_hash:
+ self.database.misc.delete_blocked_destination(other_hash)
- # Use resolved identity hash or fallback to destination hash
- target_hash = identity_hash or destination_hash
- dest_bytes = bytes.fromhex(target_hash)
+ # Always remove from Reticulum blackhole if available
+ try:
+ if hasattr(self, "reticulum") and self.reticulum:
+ identity_hash = None
+ announce = self.database.announces.get_announce_by_hash(
+ destination_hash,
+ )
+ if announce and announce.get("identity_hash"):
+ identity_hash = announce["identity_hash"]
- if hasattr(self.reticulum, "unblackhole_identity"):
- self.reticulum.unblackhole_identity(dest_bytes)
- except Exception as e:
- print(f"Failed to unblackhole identity in Reticulum: {e}")
+ target_hash = identity_hash or destination_hash
+ dest_bytes = bytes.fromhex(target_hash)
+
+ if hasattr(self.reticulum, "unblackhole_identity"):
+ self.reticulum.unblackhole_identity(dest_bytes)
+ except Exception as e:
+ print(f"Failed to unblackhole identity in Reticulum: {e}")
+
+ AsyncUtils.run_async(self._broadcast_blocked_destinations())
return web.json_response({"message": "ok"})
except Exception as e:
@@ -14863,6 +14896,27 @@ class ReticulumMeshChat:
),
)
+ async def _broadcast_blocked_destinations(self):
+ try:
+ blocked = self.database.misc.get_blocked_destinations()
+ blocked_list = [
+ {
+ "destination_hash": b["destination_hash"],
+ "created_at": b["created_at"],
+ }
+ for b in blocked
+ ]
+ await self.websocket_broadcast(
+ json.dumps(
+ {
+ "type": "blocked_destinations",
+ "blocked_destinations": blocked_list,
+ },
+ ),
+ )
+ except Exception as e:
+ print(f"_broadcast_blocked_destinations: failed: {e}")
+
# returns a dictionary of config
def get_config_dict(self, context=None):
ctx = context or self.current_context
@@ -15408,40 +15462,47 @@ class ReticulumMeshChat:
if not ctx or not ctx.database:
return False
try:
- return ctx.database.misc.is_destination_blocked(destination_hash)
+ if ctx.database.misc.is_destination_blocked(destination_hash):
+ return True
+ # Check if any destination for this identity is blocked
+ announce = ctx.database.announces.get_announce_by_hash(destination_hash)
+ if announce and announce.get("identity_hash"):
+ identity_hash = announce["identity_hash"]
+ other_announces = ctx.database.announces.get_announces_by_identity_hash(
+ identity_hash
+ )
+ for other in other_announces:
+ if ctx.database.misc.is_destination_blocked(
+ other["destination_hash"]
+ ):
+ return True
+ return False
except Exception:
return False
def _lxmf_reticulum_enforce_block(self, destination_hash: str) -> None:
"""Apply Reticulum blackhole or drop_path after a peer was added to the block list."""
- if self.config.blackhole_integration_enabled.get():
- try:
- if hasattr(self, "reticulum") and self.reticulum:
- identity_hash = None
- announce = self.database.announces.get_announce_by_hash(
- destination_hash,
+ try:
+ if hasattr(self, "reticulum") and self.reticulum:
+ identity_hash = None
+ announce = self.database.announces.get_announce_by_hash(
+ destination_hash,
+ )
+ if announce and announce.get("identity_hash"):
+ identity_hash = announce["identity_hash"]
+ target_hash = identity_hash or destination_hash
+ dest_bytes = bytes.fromhex(target_hash)
+ if hasattr(self.reticulum, "blackhole_identity"):
+ reason = (
+ f"Blocked in MeshChatX (from {destination_hash})"
+ if identity_hash
+ else "Blocked in MeshChatX"
)
- if announce and announce.get("identity_hash"):
- identity_hash = announce["identity_hash"]
- target_hash = identity_hash or destination_hash
- dest_bytes = bytes.fromhex(target_hash)
- if hasattr(self.reticulum, "blackhole_identity"):
- reason = (
- f"Blocked in MeshChatX (from {destination_hash})"
- if identity_hash
- else "Blocked in MeshChatX"
- )
- self.reticulum.blackhole_identity(dest_bytes, reason=reason)
- else:
- self.reticulum.drop_path(dest_bytes)
- except Exception as e:
- print(f"_lxmf_reticulum_enforce_block: blackhole failed: {e}")
- else:
- try:
- if hasattr(self, "reticulum") and self.reticulum:
- self.reticulum.drop_path(bytes.fromhex(destination_hash))
- except Exception as e:
- print(f"_lxmf_reticulum_enforce_block: drop_path failed: {e}")
+ self.reticulum.blackhole_identity(dest_bytes, reason=reason)
+ else:
+ self.reticulum.drop_path(dest_bytes)
+ except Exception as e:
+ print(f"_lxmf_reticulum_enforce_block: failed: {e}")
def banish_lxmf_peer(self, destination_hash: str, context=None) -> None:
"""Banish (block) an LXMF peer: persist block and apply Reticulum blackhole/drop when configured."""
@@ -15452,10 +15513,23 @@ class ReticulumMeshChat:
return
try:
ctx.database.misc.add_blocked_destination(destination_hash)
+ # Block all known destinations for the same identity
+ announce = ctx.database.announces.get_announce_by_hash(destination_hash)
+ if announce and announce.get("identity_hash"):
+ identity_hash = announce["identity_hash"]
+ other_announces = ctx.database.announces.get_announces_by_identity_hash(
+ identity_hash
+ )
+ for other in other_announces:
+ other_hash = other["destination_hash"]
+ if other_hash != destination_hash:
+ ctx.database.misc.add_blocked_destination(other_hash)
+ self._lxmf_reticulum_enforce_block(other_hash)
except Exception as e:
- print(f"banish_lxmf_peer: add_blocked_destination failed: {e}")
+ print(f"banish_lxmf_peer: failed: {e}")
return
self._lxmf_reticulum_enforce_block(destination_hash)
+ AsyncUtils.run_async(self._broadcast_blocked_destinations())
def check_spam_keywords(self, title: str, content: str, context=None) -> bool:
"""Return whether title/content match configured spam keywords."""

diff --git a/meshchatx/src/backend/database/announces.py b/meshchatx/src/backend/database/announces.py
index 92863909..289cc861 100644
--- a/meshchatx/src/backend/database/announces.py
+++ b/meshchatx/src/backend/database/announces.py
@@ -105,6 +105,12 @@ class AnnounceDAO:
(destination_hash,),
)
+ def get_announces_by_identity_hash(self, identity_hash):
+ return self.provider.fetchall(
+ "SELECT * FROM announces WHERE identity_hash = ?",
+ (identity_hash,),
+ )
+
def get_announce_count_by_aspect(self, aspect):
row = self.provider.fetchone(
"SELECT COUNT(*) as count FROM announces WHERE aspect = ?",

diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue
index 38a6a0e1..dabca054 100644
--- a/meshchatx/src/frontend/components/App.vue
+++ b/meshchatx/src/frontend/components/App.vue
@@ -1247,6 +1247,10 @@ export default {
this.updateTelephoneStatus();
break;
}
+ case "blocked_destinations": {
+ GlobalState.blockedDestinations = json.blocked_destinations || [];
+ break;
+ }
case "lxmf.delivery": {
if (this.config?.do_not_disturb_enabled) {
break;


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────